/* Funky instruction set datatype contains the extension to the function
 * name which will be called (in local window[] scope) and the 'real'
 * opcode in case of substitutions, as well as max number of bytes that
 * the function takes in storage - this is important as some of the
 * opcodes (ie: JMP) assign absolute values to the program counter so
 * we can't blindly assume that we must bump the PC every time
 */
var instructionSet = new Array();

// we do a lot of these
function decode_address(addr) {
	return(parseInt(get_addr(addr + 1)) + parseInt((get_addr(addr + 2) * 10)));
}

// Break from program to monitor
instructionSet[0] = { "opcode": 0, "mnemonic": "BRK", "bytes": 0 };
function execute_BRK(bytes) {
	runMode = 0;
	halt_program("READY");
	update_status();
	show_pc();
	return(bytes);
}

// Load accumulator from location xx
instructionSet[1] = { "opcode": 1, "mnemonic": "LDA", "bytes": 3 };
function execute_LDA(bytes) {
	var addr = decode_address(programCounter);
	theAccumulator = get_addr(addr);
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Store accumulator into location xx
instructionSet[2] = { "opcode": 2, "mnemonic": "STA", "bytes": 3 };
function execute_STA(bytes) {
	var addr = decode_address(programCounter);
	set_addr(addr, theAccumulator);
	return(bytes);
}

// Clear carry (to zero)
instructionSet[3] = { "opcode": 3, "mnemonic": "CLC", "bytes": 1 };
function execute_CLC(bytes) {
	carryFlag = 0;
	return(bytes);
}

// Add contents of location xx to accumulator with carry
instructionSet[4] = { "opcode": 4, "mnemonic": "ADC", "bytes": 3 };
function execute_ADC(bytes) {
	var addr = decode_address(programCounter);
	theAccumulator += get_addr(addr);
	theAccumulator += carryFlag;
	update_carry_flag(theAccumulator);
	theAccumulator = theAccumulator % 10;
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Decrement the contents of location xx
instructionSet[5] = { "opcode": 5, "mnemonic": "DEC", "bytes": 3 };
function execute_DEC(bytes) {
	var addr = decode_address(programCounter);
	var value = get_addr(addr);
	value--;
	update_zero_flag(value);
	if (value < 0) {
		value = 9;
	}
	set_addr(addr, value);
	return(bytes);
}

// Increment the contents of location xx
instructionSet[6] = { "opcode": 6, "mnemonic": "INC", "bytes": 3 };
function execute_INC(bytes) {
	var addr = decode_address(programCounter);
	var value = get_addr(addr);
	value++;
	update_carry_flag(value);
	value = value % 10;
	update_zero_flag(value);
	set_addr(addr, value);
	return(bytes);
}

// Move immediate value into accumulator
instructionSet[7] = { "opcode": 7, "mnemonic": "MOV", "bytes": 2 };
function execute_MOV(bytes) {
	theAccumulator = get_addr(programCounter + 1);
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Jump to location xx
instructionSet[8] = { "opcode": 8, "mnemonic": "JMP", "bytes": 3 };
function execute_JMP(bytes) {
	var addr = decode_address(programCounter);
	return(addr - programCounter);
}

// Jump to location xx if previous result != 0 (zero flag clear)
instructionSet[9] = { "opcode": 9, "mnemonic": "JNE", "bytes": 3 };
function execute_JNE(bytes) {
	if (0 == zeroFlag) {
		var addr = decode_address(programCounter);
		bytes = addr - programCounter;
	}
	return(bytes);
}

// optional replacement instructions (10-19)

// Set carry (to one)
instructionSet[10] = { "opcode": 10, "mnemonic": "SEC", "bytes": 1 };
function execute_SEC(bytes) {
	carryFlag = 1;
	return(bytes);
}

// Subtract contents of xx from accumulator with carry
instructionSet[11] = { "opcode": 11, "mnemonic": "SBC", "bytes": 3 };
function execute_SBC(bytes) {
	var addr = decode_address(programCounter);
	var value = get_addr(addr);
	theAccumulator -= value;
	theAccumulator -= carryFlag;
	if (theAccumulator < 0) {
		theAccumulator = 10 + theAccumulator;
	}
	update_zero_flag(theAccumulator);
	return(bytes);
}

//Jump to location xx if carry is clear
instructionSet[12] = { "opcode": 12, "mnemonic": "JCC", "bytes": 3 };
function execute_JCC(bytes) {
	if (0 == carryFlag) {
		var addr = decode_address(programCounter);
		bytes = addr - programCounter;
	}
	return(bytes);
}

// Jump to location xx if carry is set
instructionSet[13] = { "opcode": 13, "mnemonic": "JCS", "bytes": 3 };
function execute_JCS(bytes) {
	if (0 != carryFlag) {
		var addr = decode_address(programCounter);
		bytes = addr - programCounter;
	}
	return(bytes);
}

// Jump to location xx if zero flag is set
instructionSet[14] = { "opcode": 14, "mnemonic": "JEQ", "bytes": 3 };
function execute_JEQ(bytes) {
	if (0 != zeroFlag) {
		var addr = decode_address(programCounter);
		bytes = addr - programCounter;
	}
	return(bytes);
}

// Subtract one from the accumulator
instructionSet[15] = { "opcode": 15, "mnemonic": "DCA", "bytes": 1 };
function execute_DCA(bytes) {
	theAccumulator -= 1;
	if (theAccumulator < 0) {
		theAccumulator = 10 + theAccumulator;
	}
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Add one to the accumulator
instructionSet[16] = { "opcode": 16, "mnemonic": "INA", "bytes": 1 };
function execute_INA(bytes) {
	theAccumulator += 1;
	update_carry_flag(theAccumulator);
	theAccumulator = theAccumulator % 10;
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Compare the accumulator with immediate value (zero flag set if true, clear otherwise)
instructionSet[17] = { "opcode": 17, "mnemonic": "TST", "bytes": 2 };
function execute_TST(bytes) {
	var value = get_addr(programCounter + 1);
	update_zero_flag(theAccumulator - value);
	return(bytes);
}

// Load accumulator with the contents pointed to by address xx and xx+1
instructionSet[18] = { "opcode": 18, "mnemonic": "LDI", "bytes": 3 };
function execute_LDI(bytes) {
	var addr = decode_address(programCounter);
	var addr2 = decode_address(addr);
	theAccumulator = get_addr(addr2);
	update_zero_flag(theAccumulator);
	return(bytes);
}

// Store accumulator into the address pointed to by xx and xx+1
instructionSet[19] = { "opcode": 19, "mnemonic": "STI", "bytes": 3 };
function execute_STI(bytes) {
	var addr = decode_address(programCounter);
	var addr2 = decode_address(addr);
	set_value(addr2, theAccumulator);
	return(bytes);
}


// sensible default values
var programCounter = 0;
var carryFlag = 0;
var zeroFlag = 0;
var theAccumulator = 0;
var activeInstructions = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
var runMode = 0;
var fastMode = 0;
var inputPort = 0;
var outputPort = 0;
var errorState = "READY";

var HIMEM = 80;

// not just the Accumulator, but also other tests
function update_zero_flag(testValue) {
	if (0 == testValue) {
		zeroFlag = 1;
	} else {
		zeroFlag = 0;
	}
}

// again, not always the accumulator
function update_carry_flag(testValue) {
	if (testValue > 9) {
		carryFlag = 1;
	} else {
		carryFlag = 0;
	}
}

function get_addr(address) {
	var loc = address;
	if (loc < 10)
		loc = "0" + loc;
	return parseInt($('#' + loc).text());
}

function set_addr(address, value) {
	var loc = address;
	if (loc < 10)
		loc = "0" + loc;
	$('#' + loc).text(value);
}

function halt_program(msg) {
	if (msg.length > 0) {
		errorState = msg;
	}
	runMode = 0;
	update_status();
}
function decode_pc() {
	var opcode = activeInstructions[get_addr(programCounter)];
	var decode = instructionSet[opcode].mnemonic;
	if (2 == instructionSet[opcode].bytes) {
		decode += " " + get_addr(programCounter + 1);
	}
	if (3 == instructionSet[opcode].bytes) {
		var addr = parseInt(get_addr(programCounter + 2) * 10);
		addr += parseInt(get_addr(programCounter + 1));
		decode += " " + addr;
	}
	$('#mnemonic').text(decode);
}
function show_pc() {
	var cellID;
	if (programCounter < 10) {
		cellID = "#0" + programCounter;
	} else {
		cellID = "#" + programCounter;
	}
	$('.peekotable').find('td').removeClass('highlight');
	$(cellID).addClass('highlight');
}
function show_accumulator() {
	$('#accumulator').text(theAccumulator);
}
function show_carry() {
	$('#carry').text(carryFlag);
}
function show_zero() {
	$('#zero').text(zeroFlag);
}
function show_state() {
	$('#state').text(errorState);
}
function show_activity() {
	if (0 == runMode) {
		$('#activity').text("STOP");
	} else {
		$('#activity').text("RUN");
	}
}
function update_status() {
	show_state();
	show_activity();
	show_accumulator();
	show_carry();
	show_zero();
	decode_pc();
}

function get_instruction_set(html) {
	var fullSet, pre, post;
	if (html) {
		fullSet = '<ol start="0" class="instructionlist">';
		pre = "<li>";
		post = "</li>";
	} else {
		fullSet = "";
		pre = "";
		post = "\n";
	}
	for (var i in activeInstructions) {
		fullSet += pre + instructionSet[activeInstructions[i]].mnemonic + post ;
	}
	if (html) {
		fullSet += "</ol>";
	}
	return(fullSet);
}

function get_all_instructions(html) {
	var fullSet, pre, post;
	if (html) {
		fullSet = '<ol start="0" class="instructionset">';
		pre = "<li>";
		post = "</li>";
	} else {
		fullSet = "";
		pre = "";
		post = "\n";
	}
	for (var i in instructionSet) {
		fullSet += pre + instructionSet[i].mnemonic + post;
	}
	if (html) {
		fullSet += "</ol>";
	}
	return(fullSet);
}

function change_instruction(s, d) {
	if ((s != null) && (d != null)) {
		activeInstructions[s] = d;
	}
}

function set_pc(pc) {
	if (pc >= HIMEM) {
		halt_program("ERROR");
		pc = 0;
	}
	programCounter = pc % HIMEM;
}

function change_pc(diff) {
	programCounter += diff;
	if (programCounter < 0)
		programCounter += HIMEM;
	programCounter = programCounter % HIMEM;
}

function execute_pc() {
	var opcode;

	if (programCounter >= HIMEM) {
		halt_program("ERROR");
		return;
	}

	opcode = activeInstructions[get_addr(programCounter)];
	programCounter += window["execute_" + instructionSet[opcode].mnemonic](instructionSet[opcode].bytes);
}

function save_state(title, description) {
	var mem = new Array();
	for (var hi=0; hi < 8; hi++) {
		for (var lo=0; lo<10; lo++) {
			mem.push($('#'+hi+lo).text());
		}
	}
	var theState = { "title": title,
			"description": description,
			"activeInstructions": JSON.stringify(activeInstructions),
			"memory": JSON.stringify(mem)
		};
	var allCode = JSON.parse(localStorage.getItem('peekoComputer'));
	if (null == allCode) {
		allCode = new Array();
	}
	var replaced = false;
	for (var i in allCode) {
		if (allCode[i].title == title) {
			allCode[i] = theState;
			replaced = true;
		}
	}
	if (false == replaced) {
		allCode.push(theState);
	}
	localStorage.setItem('peekoComputer', JSON.stringify(allCode));
}

function load_state() {
	halt_program("READY");
	activeInstructions = JSON.parse(localStorage.getItem('activeInstructions'));
	var mem = JSON.parse(localStorage.getItem('memory'));
	for (var hi=0; hi < 8; hi++) {
		for (var lo=0; lo<10; lo++) {
			$('#'+hi+lo).text(mem.shift());
		}
	}
	$('#instset').html(get_instruction_set(true));
	set_pc(0);
	update_status();
	show_pc();
}
